import fetch from "node-fetch";
import { prisma } from "../database/generated/prisma-client";
import { blkGraphqlFragment, liteBlkGraphqlFragment } from "./utils";
import logger from "./log";

class Courier {
    constructor({
        name,
        networkIds = [],
        url,
        auth: { authUrl, userId, password, token = { type: "", value: "" } } = {},
        checkTimeInterval = 30000,
        timeout = 5000,
        lite = false,
        startingBlockHeight = 1,
        endingBlockHeight = null,
        deliverySuccess = (res) => [res.status === 200, res.statusText],
        isUnauthorized = (res) => res.status === 401,
        signIn = () => Promise.resolve({ type: "", value: "" }),
        alterBlock = (block) => block,
    }) {
        this.isBusy = false;
        this.name = name;
        this.networkIds = networkIds;
        this.url = url;
        this.token = token;
        this.authUrl = authUrl;
        this.userId = userId;
        this.password = password;
        this.timeInterval = checkTimeInterval;
        this.timeout = timeout;
        this.lite = lite;
        this.startingBlockHeight = startingBlockHeight;
        this.endingBlockHeight = endingBlockHeight;
        this.deliverySuccess = deliverySuccess;
        this.isUnauthorized = isUnauthorized;
        this.signIn = signIn;
        this.alterBlock = alterBlock;
    }

    getAuthorization() {
        logger.info(`For subscriber ${this.name}: try to get the authorization from auth url: ${this.authUrl}`);
        this.signIn(this.authUrl, this.userId, this.password)
            .then((token) => {
                logger.info(`For subscriber ${this.name}: signIn successfully and got the token`);
                this.token = token;
            }).catch((err) => {
                logger.error(`For subscriber ${this.name}: failed to signIn with the error: ${err}`);
            });
    }

    async init(blockchainId) {
        setInterval(() => {
            this.handleUndeliveredBlocks(blockchainId);
        }, this.timeInterval);

        await prisma.upsertSubscriber({
            where: { name: this.name },
            update: { url: this.url, interestingBlockHeightRange: `${this.startingBlockHeight}-${this.endingBlockHeight}` },
            create: {
                name: this.name,
                url: this.url,
                blockchainIds: { set: this.networkIds },
                latestReceivedBlockHeights: { set: this.networkIds.map(() => 0) },
                interestingBlockHeightRange: `${this.startingBlockHeight}-${this.endingBlockHeight}`
            },
        }).then(() => {
            logger.info(`For subscriber ${this.name}(${this.url}): success init a courier for blockchain network: ${blockchainId}`);
        }).catch((err) => {
            logger.error(`For subscriber ${this.name}(${this.url}): failed to init a exclusive courier`, err);
        });
        if ((!this.token || !this.token.value) && this.authUrl) {
            logger.info(`For subscriber ${this.name}: try to get authorization`);
            this.getAuthorization();
        }
    }

    async handleUndeliveredBlocks(blockchainId) {
        logger.info(`For subscriber ${this.name}: try to handle the undelivered blocks`);
        if (this.isBusy) {
            logger.info(`For subscriber ${this.name}: still in busy so wait to handle the undelivered blocks`);
            return;
        }
        this.isBusy = true;
        logger.info(`For subscriber ${this.name}: started handling the undelivered blocks`);
        try {
            const subscriber = await prisma.subscriber({ name: this.name });
            if (!subscriber) throw new Error(`Not found subscriber ${this.name} in local database`);
            // 已被推送成功的最高区块高度
            const { blockchainIds, latestReceivedBlockHeights } = subscriber;
            const index = blockchainIds.indexOf(blockchainId);
            const latestReceivedBlockHeight = latestReceivedBlockHeights[index]
            logger.info(`For subscriber ${this.name}: the latest received block height is ${latestReceivedBlockHeight} from blockchain network: ${blockchainId}`);
            // 检测到RepChain已产生的最高区块高度
            const { BLOCK_COUNT: blockCount = 0 } = await prisma.network({ id: blockchainId });
            logger.info(`For subscriber ${this.name}: the newest block height is ${blockCount} from blockchain network: ${blockchainId}`);
            logger.info(`For subscriber ${this.name}: the interesting block height range is: ${this.startingBlockHeight}-${this.endingBlockHeight}`);
            // 待推送区块的高度的左边界值
            const leftHeight = this.startingBlockHeight > latestReceivedBlockHeight + 1 ? this.startingBlockHeight : latestReceivedBlockHeight + 1;
            // 待推送区块的高度的右边界值
            const rightHeight = !this.endingBlockHeight ? blockCount : (this.endingBlockHeight > blockCount ? blockCount : this.endingBlockHeight);
            if (leftHeight > rightHeight) {
                this.isBusy = false;
                logger.info(`For subscriber ${this.name}: no undelivered blocks, left from handling undelivered blocks for blockchain network: ${blockchainId}`);
                return;
            }
            logger.info(`For subscriber ${this.name}: started handling the blocks(${leftHeight}-${rightHeight}) from blockchain network: ${blockchainId}`);
            for (let nextBlockHeight = leftHeight; nextBlockHeight <= rightHeight; nextBlockHeight++) {
                const blks = await prisma.blocks({ where: { HEADER: { BLOCKCHAIN: { id: blockchainId }, HEIGHT: nextBlockHeight } } }).$fragment(
                    this.lite ? liteBlkGraphqlFragment : blkGraphqlFragment,
                );
                // The result always get only one block
                const blk = blks[0];
                await this.deliver(blk);
            }
        } catch (e) {
            this.isBusy = false;
            logger.error(`For subscriber ${this.name}: left from handling undelivered blocks because of error`, e);
            return;
        }
        this.isBusy = false;
        logger.info(`For subscriber ${this.name}: finished handling the undelivered blocks`);
    }

    async handleNewBlock(block) {
        const height = block.HEADER.HEIGHT;
        const blockchainId = block.BLOCKCHAIN.id;
        logger.info(`For subscriber ${this.name}: try to handle the new block(height=${height}) from blockchain network: ${blockchainId}`);
        if (this.isBusy) {
            logger.info(`For subscriber ${this.name}: still in busy so couldn't handle the new block(height=${height}) from blockchain network: ${blockchainId}, left`);
            return;
        }
        this.isBusy = true;
        logger.info(`For subscriber ${this.name}: started handling the new block(height=${height}) from blockchain network: ${blockchainId}`);
        try {
            const subscriber = await prisma.subscriber({ name: this.name });
            if (!subscriber) throw new Error(`Not found subscriber ${this.name} in local database`);
            const { blockchainIds, latestReceivedBlockHeights } = subscriber;
            const index = blockchainIds.indexOf(blockchainId);
            const latestReceivedBlockHeight = latestReceivedBlockHeights[index];
            // 当前新区块要么是已成功推送区块的下一个，要么是指定的起始区块
            if (height === latestReceivedBlockHeight + 1
                || height === this.startingBlockHeight) {
                if (this.lite) {
                    block.TRANSACTIONS = block.TRANSACTIONS.map((tx) => ({ TXID: tx.TXID }));
                }
                await this.deliver(block);
            }
            else {
                const msg1 = `For subscriber ${this.name}: the new block(height=${height}) from blockchain network: ${blockchainId} `;
                const msg2 = "is not the next block should be delivered, so started to deliver the older blocks";
                logger.info(`${msg1}${msg2}`);
                logger.info(`For subscriber ${this.name}: started handling the blocks(${latestReceivedBlockHeight + 1}-${height}) from blockchain network: ${blockchainId}`);
                for (let nextBlockHeight = latestReceivedBlockHeight + 1; nextBlockHeight <= height; nextBlockHeight++) {
                    const blks = await prisma.blocks({ where: { HEADER: { BLOCKCHAIN: { id: blockchainId }, HEIGHT: nextBlockHeight } } }).$fragment(
                        this.lite ? liteBlkGraphqlFragment : blkGraphqlFragment,
                    );
                    // The result always get only one block
                    const blk = blks[0];
                    await this.deliver(blk);
                }
            }
        } catch (e) {
            this.isBusy = false;
            logger.error(`For subscriber ${this.name}: left from handling new block(height=${height}) from blockchain network: ${blockchainId} because of error`, e);
            return;
        }
        this.isBusy = false;
        logger.info(`For subscriber ${this.name}: finished handling the new block(height=${height}) from blockchain network: ${blockchainId}`);
    }

    // 默认假定订阅者提供了如下API
    async deliver(block) {
        const height = block.HEADER.HEIGHT;
        const blockchainId = block.BLOCKCHAIN.id;
        logger.info(`For subscriber ${this.name}: started delivering the block(height=${height}) from blockchain network: ${blockchainId}`);

        const alteredBlock = this.alterBlock(block);

        // Skip to deliver block data
        if (alteredBlock === null) {
            logger.info(`For subscriber ${this.name}: skipped to deliver the block(height=${height}) from blockchain network: ${blockchainId}`);
            const { blockchainIds, latestReceivedBlockHeights } = await prisma.subscriber({ name: this.name });
            const index = blockchainIds.indexOf(blockchainId);
            latestReceivedBlockHeights[index] = height;
            return prisma.updateSubscriber({
                where: { name: this.name },
                data: { latestReceivedBlockHeights: { set: latestReceivedBlockHeights } },
            });
        }

        return fetch(this.url, {
            method: "post",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `${this.token.type} ${this.token.value}`,
            },
            body: JSON.stringify(alteredBlock),
            timeout: this.timeout,
        }).then(async (res) => {
            const [success, info] = await this.deliverySuccess(res);
            if (!success) {
                logger.error(`For subscriber ${this.name}: falied to deliver the block(height=${height}) from blockchain network: ${blockchainId}`);
                const isUnauthorized = await this.isUnauthorized(res);
                if (isUnauthorized) {
                    this.getAuthorization();
                }
                throw new Error(info);
            }
            return res;
        }).then(async () => {
            logger.info(`For subscriber ${this.name}: delivered the block(height=${height}) from blockchain network: ${blockchainId} successfully`);
            const { blockchainIds, latestReceivedBlockHeights } = await prisma.subscriber({ name: this.name });
            const index = blockchainIds.indexOf(blockchainId);
            latestReceivedBlockHeights[index] = height;
            return prisma.updateSubscriber({
                where: { name: this.name },
                data: { latestReceivedBlockHeights: { set: latestReceivedBlockHeights } },
            });
        });
    }
}

export default Courier;